{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e6b971c0",
   "metadata": {},
   "source": [
    "# FM 广告点击率预测"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7496cf63-bdb5-4eaf-9bcf-856b53bd18ed",
   "metadata": {},
   "source": [
    "FM算法全称为因子分解机 (FactorizationMachine)。\n",
    "\n",
    "它是广告和推荐领域非常著名的算法，在线性回归模型上考虑了特征的二阶交互。\n",
    "\n",
    "适合捕捉大规模稀疏特征(类别特征)当中的特征交互。\n",
    "\n",
    "\n",
    "本范例演示继承 torchkeras.tabular.models.BaseModel 构建FM模型 进行广告点击率预测。\n",
    "\n",
    "有关FM模型的理论介绍，参考如下eat_pytorch_in_20_days中的讲解教程：\n",
    "\n",
    "https://github.com/lyhue1991/eat_pytorch_in_20_days/blob/master/7-3%2CFM%E6%A8%A1%E5%9E%8B.ipynb\n",
    "\n",
    "\n",
    "公众号**算法美食屋**后台回复关键词：torchkeras，获取本文notebook源码和所用criteo_small数据集下载链接。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa19852f",
   "metadata": {},
   "source": [
    "## 一，准备数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "85cbdc51",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.preprocessing import OrdinalEncoder\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "np.random.seed(42)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0f799967",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import LabelEncoder\n",
    "\n",
    "dfdata = pd.read_csv('criteo_small.zip',sep='\\t',header=None)\n",
    "dfdata.columns = [\"label\"] + [\"I\"+str(x) for x in range(1,14)] + [\n",
    "    \"C\"+str(x) for x in range(14,40)]\n",
    "\n",
    "target_col = 'label'\n",
    "cat_cols = [x for x in dfdata.columns if x.startswith('C')]\n",
    "num_cols = [x for x in dfdata.columns if x.startswith('I')]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6a809254",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "dftrain_val,dftest_raw = train_test_split(dfdata,test_size=0.2,random_state=42)\n",
    "dftrain_raw,dfval_raw = train_test_split(dftrain_val,test_size=0.2,random_state=42)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "2fde517f",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "2024-09-04 13:59:50.729313: I tensorflow/core/util/port.cc:110] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n",
      "2024-09-04 13:59:50.779526: I tensorflow/core/platform/cpu_feature_guard.cc:182] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.\n",
      "To enable the following instructions: AVX2 AVX512F AVX512_VNNI FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.\n",
      "2024-09-04 13:59:51.512095: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "1ee8678b862c4fa1b4e606e1026bd17b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/24 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from torchkeras.tabular import TabularPreprocessor\n",
    "from sklearn.preprocessing import OrdinalEncoder\n",
    "\n",
    "#特征工程\n",
    "pipe = TabularPreprocessor(cat_features = cat_cols, onehot_max_cat_num=3)\n",
    "encoder = OrdinalEncoder()\n",
    "\n",
    "dftrain = pipe.fit_transform(dftrain_raw.drop(target_col,axis=1))\n",
    "dftrain[target_col] = encoder.fit_transform(\n",
    "    dftrain_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n",
    "\n",
    "dfval = pipe.transform(dfval_raw.drop(target_col,axis=1))\n",
    "dfval[target_col] = encoder.transform(\n",
    "    dfval_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n",
    "\n",
    "dftest = pipe.transform(dftest_raw.drop(target_col,axis=1))\n",
    "dftest[target_col] = encoder.transform(\n",
    "    dftest_raw[target_col].values.reshape(-1,1)).astype(np.int32)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "b219ae5c",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "from torchkeras.tabular import TabularDataset\n",
    "from torch.utils.data import Dataset,DataLoader \n",
    "\n",
    "def get_dataset(dfdata):\n",
    "    return TabularDataset(\n",
    "                data = dfdata,\n",
    "                task = 'binary',\n",
    "                target = [target_col],\n",
    "                continuous_cols = pipe.get_numeric_features(),\n",
    "                categorical_cols = pipe.get_embedding_features()\n",
    "        )\n",
    "\n",
    "def get_dataloader(ds,batch_size=512,num_workers=0,shuffle=False):\n",
    "    dl = DataLoader(\n",
    "            ds,\n",
    "            batch_size=batch_size,\n",
    "            shuffle=shuffle,\n",
    "            num_workers=num_workers,\n",
    "            pin_memory=False,\n",
    "        )\n",
    "    return dl \n",
    "    \n",
    "ds_train = get_dataset(dftrain)\n",
    "ds_val = get_dataset(dfval)\n",
    "ds_test = get_dataset(dftest)\n",
    "\n",
    "dl_train = get_dataloader(ds_train,batch_size=2048,shuffle=True)\n",
    "dl_val = get_dataloader(ds_val,shuffle=False)\n",
    "dl_test = get_dataloader(ds_test,shuffle=False)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "5eef69c5",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "for batch in dl_train:\n",
    "    break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "07cf31ea",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(640000, 45)"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "dftrain.shape "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f35e558f",
   "metadata": {},
   "source": [
    "## 二，定义模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "95fbc5be-cf93-4c5f-90db-47aeef5f27f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from dataclasses import dataclass, field\n",
    "from typing import Optional\n",
    "from torchkeras.tabular.models import BaseModel\n",
    "\n",
    "\n",
    "from torchkeras.tabular.config import ModelConfig\n",
    "\n",
    "\n",
    "@dataclass\n",
    "class FMConfig(ModelConfig):\n",
    "    input_embed_dim: int = field(\n",
    "        default=32,\n",
    "        metadata={\"help\": \"The embedding dimension for the input categorical features. Defaults to 32\"},\n",
    "    )\n",
    "    \n",
    "    embedding_initialization: Optional[str] = field(\n",
    "        default=\"kaiming_uniform\",\n",
    "        metadata={\n",
    "            \"help\": \"Initialization scheme for the embedding layers. Defaults to `kaiming`\",\n",
    "            \"choices\": [\"kaiming_uniform\", \"kaiming_normal\"],\n",
    "        }\n",
    "    )\n",
    "    \n",
    "    _module_src: str = field(default=\"models.fm\")\n",
    "    _model_name: str = field(default=\"FMModel\")\n",
    "    _backbone_name: str = field(default=\"FMBackbone\")\n",
    "    _config_name: str = field(default=\"FMConfig\")\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "8b1a5adb-c7a1-42c9-8d4e-3fd3780411af",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch \n",
    "from torch import nn\n",
    "from torch import nn,Tensor \n",
    "import torch.nn.functional as F \n",
    "\n",
    "from omegaconf import DictConfig\n",
    "from typing import Dict,Any\n",
    "from torchkeras.tabular.models import BaseModel\n",
    "\n",
    "class NumEmbedding(nn.Module):\n",
    "    \"\"\"\n",
    "    连续特征用linear层编码\n",
    "    输入shape: [batch_size,features_num(n), d_in], # d_in 通常是1\n",
    "    输出shape: [batch_size,features_num(n), d_out]\n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(self, n: int, d_in: int, d_out: int, bias: bool = False) -> None:\n",
    "        super().__init__()\n",
    "        self.weight = nn.Parameter(Tensor(n, d_in, d_out))\n",
    "        self.bias = nn.Parameter(Tensor(n, d_out)) if bias else None\n",
    "        with torch.no_grad():\n",
    "            for i in range(n):\n",
    "                layer = nn.Linear(d_in, d_out)\n",
    "                self.weight[i] = layer.weight.T\n",
    "                if self.bias is not None:\n",
    "                    self.bias[i] = layer.bias\n",
    "\n",
    "    def forward(self, x_num):\n",
    "        # x_num: batch_size, features_num, d_in\n",
    "        assert x_num.ndim == 3\n",
    "        x = x_num[..., None] * self.weight[None]\n",
    "        x = x.sum(-2)\n",
    "        #x = torch.einsum(\"bfi,fij->bfj\",x_num,self.weight)\n",
    "        if self.bias is not None:\n",
    "            x = x + self.bias[None]\n",
    "        return x\n",
    "    \n",
    "class CatEmbedding(nn.Module):\n",
    "    \"\"\"\n",
    "    离散特征用Embedding层编码\n",
    "    输入shape: [batch_size,features_num], \n",
    "    输出shape: [batch_size,features_num, d_embed]\n",
    "    \"\"\"\n",
    "    def __init__(self, categories, d_embed):\n",
    "        super().__init__()\n",
    "        self.embedding = nn.Embedding(sum(categories), d_embed)\n",
    "        self.offsets = nn.Parameter(\n",
    "                torch.tensor([0] + categories[:-1]).cumsum(0),requires_grad=False)\n",
    "        \n",
    "        torch.nn.init.xavier_uniform_(self.embedding.weight.data)\n",
    "\n",
    "    def forward(self, x_cat):\n",
    "        \"\"\"\n",
    "        :param x_cat: Long tensor of size ``(batch_size, features_num)``\n",
    "        \"\"\"\n",
    "        x = x_cat + self.offsets[None]\n",
    "        return self.embedding(x) \n",
    "    \n",
    "class CatLinear(nn.Module):\n",
    "    \"\"\"\n",
    "    离散特征用Embedding实现线性层（等价于先F.onehot再nn.Linear()）\n",
    "    输入shape: [batch_size,features_num], \n",
    "    输出shape: [batch_size,d_out]\n",
    "    \"\"\"\n",
    "    def __init__(self, categories, d_out=1):\n",
    "        super().__init__()\n",
    "        self.fc = nn.Embedding(sum(categories), d_out)\n",
    "        self.bias = nn.Parameter(torch.zeros((d_out,)))\n",
    "        self.offsets = nn.Parameter(\n",
    "                torch.tensor([0] + categories[:-1]).cumsum(0),requires_grad=False)\n",
    "\n",
    "    def forward(self, x_cat):\n",
    "        \"\"\"\n",
    "        :param x: Long tensor of size ``(batch_size, features_num)``\n",
    "        \"\"\"\n",
    "        x = x_cat + self.offsets[None]\n",
    "        return torch.sum(self.fc(x), dim=1) + self.bias \n",
    "    \n",
    "    \n",
    "class FMLayer(nn.Module):\n",
    "    \"\"\"\n",
    "    FM交互项\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(self, reduce_sum=True):\n",
    "        super().__init__()\n",
    "        self.reduce_sum = reduce_sum\n",
    "\n",
    "    def forward(self, x): #注意：这里的x是公式中的 <v_i> * xi\n",
    "        \"\"\"\n",
    "        :param x: Float tensor of size ``(batch_size, num_features, k)``\n",
    "        \"\"\"\n",
    "        square_of_sum = torch.sum(x, dim=1) ** 2\n",
    "        sum_of_square = torch.sum(x ** 2, dim=1)\n",
    "        ix = square_of_sum - sum_of_square\n",
    "        if self.reduce_sum:\n",
    "            ix = torch.sum(ix, dim=1, keepdim=True)\n",
    "        return 0.5 * ix\n",
    "\n",
    "\n",
    "\n",
    "class FMBackbone(nn.Module):\n",
    "    def __init__(self, config: DictConfig, **kwargs):\n",
    "        super().__init__()\n",
    "        self.hparams = config\n",
    "        d_numerical = self.hparams.continuous_dim\n",
    "        d_embed = self.hparams.input_embed_dim\n",
    "        \n",
    "        categories = self.hparams.categorical_cardinality\n",
    "        n_classes = self.hparams.output_dim \n",
    "        \n",
    "        self.num_linear = nn.Linear(d_numerical,n_classes) if d_numerical else None\n",
    "        self.cat_linear = CatLinear(categories,n_classes) if categories else None\n",
    "        \n",
    "        self.num_embedding = NumEmbedding(d_numerical,1, d_embed) if d_numerical else None\n",
    "        self.cat_embedding = CatEmbedding(categories, d_embed) if categories else None\n",
    "\n",
    "        self.fm = FMLayer(reduce_sum=False)\n",
    "        self.fm_linear = nn.Linear(d_embed,n_classes)\n",
    "        \n",
    "\n",
    "    def forward(self, x: Dict):\n",
    "        \n",
    "        x_cat,x_num = x[\"categorical\"], x[\"continuous\"]\n",
    "        \n",
    "    \n",
    "        #linear部分\n",
    "        x = 0.0\n",
    "        if self.num_linear:\n",
    "            x = x + self.num_linear(x_num) \n",
    "        if self.cat_linear:\n",
    "            x = x + self.cat_linear(x_cat)\n",
    "        \n",
    "        #交叉项部分\n",
    "        x_embedding = []\n",
    "        if self.num_embedding:\n",
    "            x_embedding.append(self.num_embedding(x_num[...,None]))\n",
    "        if self.cat_embedding:\n",
    "            x_embedding.append(self.cat_embedding(x_cat))\n",
    "        x_embedding = torch.cat(x_embedding,dim=1)\n",
    "\n",
    "        x = x + self.fm_linear(self.fm(x_embedding)) \n",
    "        \n",
    "        return x\n",
    "\n",
    "\n",
    "class FMModel(BaseModel):\n",
    "    def __init__(self, config: DictConfig, **kwargs):\n",
    "        super().__init__(config, **kwargs)\n",
    "\n",
    "    @property\n",
    "    def backbone(self):\n",
    "        return self._backbone\n",
    "\n",
    "    @property\n",
    "    def embedding_layer(self):\n",
    "        return self._embedding_layer\n",
    "\n",
    "    @property\n",
    "    def head(self):\n",
    "        return self._head\n",
    "        \n",
    "    def _build_network(self):\n",
    "        self._embedding_layer = nn.Identity()\n",
    "        self._backbone = FMBackbone(self.hparams)\n",
    "        setattr(self.backbone, \"output_dim\", self.hparams.output_dim)\n",
    "        self._head = nn.Identity()\n",
    "        \n",
    "    def forward(self, x: Dict) -> Dict[str, Any]:\n",
    "        x = self.embed_input(x)\n",
    "        x = self.compute_backbone(x)\n",
    "        return self.compute_head(x)\n",
    "\n",
    "    def extract_embedding(self):\n",
    "        raise ValueError(\"Extracting Embeddings is not supported by FMModel.\")\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "816ad379-8854-4239-9c45-5eb1cabbdbdb",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "input_embed_dim =  32\n",
      "\n",
      " categorical_cardinality =  [1000, 529, 1000, 1000, 246, 15, 1000, 502, 1000, 1000, 1000, 1000, 26, 1000, 1000, 10, 1000, 1000, 1000, 16, 15, 1000, 63, 1000]\n",
      "\n",
      " embedding_dims =  [[1000, 50], [529, 50], [1000, 50], [1000, 50], [246, 50], [15, 8], [1000, 50], [502, 50], [1000, 50], [1000, 50], [1000, 50], [1000, 50], [26, 13], [1000, 50], [1000, 50], [10, 5], [1000, 50], [1000, 50], [1000, 50], [16, 8], [15, 8], [1000, 50], [63, 32], [1000, 50]]\n"
     ]
    }
   ],
   "source": [
    "\n",
    "from torch import nn \n",
    "\n",
    "model_config = FMConfig(\n",
    "    task=\"binary\"\n",
    ")\n",
    "config = model_config.merge_dataset_config(ds_train)\n",
    "\n",
    "print('input_embed_dim = ', config.input_embed_dim)\n",
    "print('\\n categorical_cardinality = ',config.categorical_cardinality)\n",
    "print('\\n embedding_dims = ' , config.embedding_dims)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "784129eb-295c-45f5-8cfe-a2b099a654cf",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "ef0e5773-4f8c-4d84-b64d-cd98e67d1dbb",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "net = FMModel(config = config)\n",
    "\n",
    "#初始化参数\n",
    "net.reset_weights()\n",
    "net.data_aware_initialization(dl_train)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "e6643562-3482-4e24-863e-d8b95cd340ae",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "1"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net.backbone.output_dim "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "63483634-1b17-49ce-bab9-dda099a3a46d",
   "metadata": {},
   "outputs": [],
   "source": [
    "for batch in dl_train:\n",
    "    break "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "db727a39",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tensor(5.0227, grad_fn=<AddBackward0>)\n"
     ]
    }
   ],
   "source": [
    "output = net.forward(batch)\n",
    "loss = net.compute_loss(output,batch['target'])\n",
    "print(loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d339957",
   "metadata": {},
   "source": [
    "## 三，训练模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "c4b24884",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "from torchkeras import KerasModel \n",
    "from torchkeras.tabular import StepRunner \n",
    "KerasModel.StepRunner = StepRunner \n",
    "\n",
    "import torch \n",
    "from torchkeras.metrics import AUC \n",
    "\n",
    "optimizer = torch.optim.AdamW(net.parameters(),lr = 1e-3)\n",
    "#scheduler = torch.optim.lr_scheduler.OneCycleLR(optimizer, max_lr=0.01, \n",
    "#                                                steps_per_epoch=len(dl_train), epochs=20)\n",
    "\n",
    "keras_model = KerasModel(net,\n",
    "                   loss_fn=None,\n",
    "                   optimizer = optimizer,\n",
    "                   metrics_dict = {'auc':AUC()}\n",
    "                   )\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "48077708",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "99d5e7d0",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Detected kernel version 5.4.186, which is below the recommended minimum of 5.5.0; this can cause the process to hang. It is recommended to upgrade the kernel to the minimum version or higher.\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31m<<<<<< ⚡️ cuda is used >>>>>>\u001b[0m\n"
     ]
    },
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 600x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "<style>\n",
       "    /* background: */\n",
       "    progress::-webkit-progress-bar {background-color: #CDCDCD; width: 100%;}\n",
       "    progress {background-color: #CDCDCD;}\n",
       "\n",
       "    /* value: */\n",
       "    progress::-webkit-progress-value {background-color: #00BFFF  !important;}\n",
       "    progress::-moz-progress-bar {background-color: #00BFFF  !important;}\n",
       "    progress {color: #00BFFF ;}\n",
       "\n",
       "    /* optional */\n",
       "    .progress-bar-interrupted, .progress-bar-interrupted::-webkit-progress-bar {\n",
       "        background: #000000;\n",
       "    }\n",
       "</style>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "\n",
       "    <div>\n",
       "      <progress value='28' class='progress-bar-interrupted' max='30' style='width:300px; height:20px; vertical-align: middle;'></progress>\n",
       "      93.33% [28/30] [36:22<02:35]\n",
       "      <br>\n",
       "      ████████████████████100.00% [313/313] [val_loss=0.4867, val_auc=0.7627]\n",
       "    </div>\n",
       "    "
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[0;31m<<<<<< val_auc without improvement in 5 epoch,early stopping >>>>>> \n",
      "\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>epoch</th>\n",
       "      <th>train_loss</th>\n",
       "      <th>train_auc</th>\n",
       "      <th>lr</th>\n",
       "      <th>val_loss</th>\n",
       "      <th>val_auc</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>1.665471</td>\n",
       "      <td>0.583689</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.863944</td>\n",
       "      <td>0.644433</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2</td>\n",
       "      <td>0.759401</td>\n",
       "      <td>0.664010</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.679781</td>\n",
       "      <td>0.677369</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>3</td>\n",
       "      <td>0.630101</td>\n",
       "      <td>0.694130</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.599693</td>\n",
       "      <td>0.698997</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>4</td>\n",
       "      <td>0.569130</td>\n",
       "      <td>0.715201</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.557976</td>\n",
       "      <td>0.716136</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>5</td>\n",
       "      <td>0.536210</td>\n",
       "      <td>0.730260</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.533926</td>\n",
       "      <td>0.726223</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>6</td>\n",
       "      <td>0.515892</td>\n",
       "      <td>0.741998</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.519773</td>\n",
       "      <td>0.736829</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>7</td>\n",
       "      <td>0.502368</td>\n",
       "      <td>0.750980</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.510090</td>\n",
       "      <td>0.741529</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>8</td>\n",
       "      <td>0.493002</td>\n",
       "      <td>0.757988</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.503360</td>\n",
       "      <td>0.745799</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>9</td>\n",
       "      <td>0.486127</td>\n",
       "      <td>0.763524</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.497394</td>\n",
       "      <td>0.750396</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>10</td>\n",
       "      <td>0.480779</td>\n",
       "      <td>0.768450</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.493505</td>\n",
       "      <td>0.751792</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>11</td>\n",
       "      <td>0.476380</td>\n",
       "      <td>0.772497</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.490818</td>\n",
       "      <td>0.756171</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>12</td>\n",
       "      <td>0.473001</td>\n",
       "      <td>0.775638</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.491616</td>\n",
       "      <td>0.757831</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>13</td>\n",
       "      <td>0.469861</td>\n",
       "      <td>0.778762</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.486331</td>\n",
       "      <td>0.761165</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>14</td>\n",
       "      <td>0.466996</td>\n",
       "      <td>0.781714</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483923</td>\n",
       "      <td>0.761804</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>15</td>\n",
       "      <td>0.464711</td>\n",
       "      <td>0.784156</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483773</td>\n",
       "      <td>0.760908</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>16</td>\n",
       "      <td>0.462643</td>\n",
       "      <td>0.786244</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.481654</td>\n",
       "      <td>0.765761</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>17</td>\n",
       "      <td>0.460586</td>\n",
       "      <td>0.788467</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.481190</td>\n",
       "      <td>0.766020</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>18</td>\n",
       "      <td>0.458631</td>\n",
       "      <td>0.790648</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483011</td>\n",
       "      <td>0.765012</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>19</td>\n",
       "      <td>0.456792</td>\n",
       "      <td>0.792673</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.481545</td>\n",
       "      <td>0.764120</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>20</td>\n",
       "      <td>0.454851</td>\n",
       "      <td>0.794769</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.481539</td>\n",
       "      <td>0.766154</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>21</td>\n",
       "      <td>0.453075</td>\n",
       "      <td>0.796787</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.480574</td>\n",
       "      <td>0.767124</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>22</td>\n",
       "      <td>0.450887</td>\n",
       "      <td>0.799165</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483848</td>\n",
       "      <td>0.766299</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>22</th>\n",
       "      <td>23</td>\n",
       "      <td>0.448981</td>\n",
       "      <td>0.801278</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483397</td>\n",
       "      <td>0.767193</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>23</th>\n",
       "      <td>24</td>\n",
       "      <td>0.447010</td>\n",
       "      <td>0.803447</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.482052</td>\n",
       "      <td>0.764707</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>24</th>\n",
       "      <td>25</td>\n",
       "      <td>0.445229</td>\n",
       "      <td>0.805405</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.483392</td>\n",
       "      <td>0.764684</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>25</th>\n",
       "      <td>26</td>\n",
       "      <td>0.443187</td>\n",
       "      <td>0.807622</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.484760</td>\n",
       "      <td>0.765693</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>26</th>\n",
       "      <td>27</td>\n",
       "      <td>0.441057</td>\n",
       "      <td>0.809815</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.484869</td>\n",
       "      <td>0.763894</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>27</th>\n",
       "      <td>28</td>\n",
       "      <td>0.439072</td>\n",
       "      <td>0.811966</td>\n",
       "      <td>0.001</td>\n",
       "      <td>0.486732</td>\n",
       "      <td>0.762720</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    epoch  train_loss  train_auc     lr  val_loss   val_auc\n",
       "0       1    1.665471   0.583689  0.001  0.863944  0.644433\n",
       "1       2    0.759401   0.664010  0.001  0.679781  0.677369\n",
       "2       3    0.630101   0.694130  0.001  0.599693  0.698997\n",
       "3       4    0.569130   0.715201  0.001  0.557976  0.716136\n",
       "4       5    0.536210   0.730260  0.001  0.533926  0.726223\n",
       "5       6    0.515892   0.741998  0.001  0.519773  0.736829\n",
       "6       7    0.502368   0.750980  0.001  0.510090  0.741529\n",
       "7       8    0.493002   0.757988  0.001  0.503360  0.745799\n",
       "8       9    0.486127   0.763524  0.001  0.497394  0.750396\n",
       "9      10    0.480779   0.768450  0.001  0.493505  0.751792\n",
       "10     11    0.476380   0.772497  0.001  0.490818  0.756171\n",
       "11     12    0.473001   0.775638  0.001  0.491616  0.757831\n",
       "12     13    0.469861   0.778762  0.001  0.486331  0.761165\n",
       "13     14    0.466996   0.781714  0.001  0.483923  0.761804\n",
       "14     15    0.464711   0.784156  0.001  0.483773  0.760908\n",
       "15     16    0.462643   0.786244  0.001  0.481654  0.765761\n",
       "16     17    0.460586   0.788467  0.001  0.481190  0.766020\n",
       "17     18    0.458631   0.790648  0.001  0.483011  0.765012\n",
       "18     19    0.456792   0.792673  0.001  0.481545  0.764120\n",
       "19     20    0.454851   0.794769  0.001  0.481539  0.766154\n",
       "20     21    0.453075   0.796787  0.001  0.480574  0.767124\n",
       "21     22    0.450887   0.799165  0.001  0.483848  0.766299\n",
       "22     23    0.448981   0.801278  0.001  0.483397  0.767193\n",
       "23     24    0.447010   0.803447  0.001  0.482052  0.764707\n",
       "24     25    0.445229   0.805405  0.001  0.483392  0.764684\n",
       "25     26    0.443187   0.807622  0.001  0.484760  0.765693\n",
       "26     27    0.441057   0.809815  0.001  0.484869  0.763894\n",
       "27     28    0.439072   0.811966  0.001  0.486732  0.762720"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keras_model.fit(\n",
    "    train_data = dl_train,\n",
    "    val_data= dl_val,\n",
    "    ckpt_path='checkpoint',\n",
    "    epochs=30,\n",
    "    patience=5,\n",
    "    monitor=\"val_auc\", \n",
    "    mode=\"max\",\n",
    "    plot = True,\n",
    "    wandb = False\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26147264-b2c2-4da6-986a-b721d1373c5d",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "bec9f969",
   "metadata": {},
   "source": [
    "## 四，评估模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "60a0d622",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████| 313/313 [00:53<00:00,  5.89it/s, val_auc=0.807, val_loss=0.446]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'val_loss': 0.44630903557847484, 'val_auc': 0.8065065741539001}"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keras_model.evaluate(dl_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d6d68615",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████| 313/313 [00:14<00:00, 21.69it/s, val_auc=0.767, val_loss=0.483]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'val_loss': 0.4833966428860308, 'val_auc': 0.7671929597854614}"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keras_model.evaluate(dl_val)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "6de6ee16",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100%|██████████████████████████████| 391/391 [00:18<00:00, 21.68it/s, val_auc=0.766, val_loss=0.485]\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'val_loss': 0.48469858866213533, 'val_auc': 0.7656974196434021}"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "keras_model.evaluate(dl_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e785f2fa",
   "metadata": {},
   "source": [
    "## 五，使用模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "4e55e8fc",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 391/391 [00:02<00:00, 170.37it/s]\n"
     ]
    }
   ],
   "source": [
    "from tqdm import tqdm \n",
    "\n",
    "net,dl_test = keras_model.accelerator.prepare(net,dl_test)\n",
    "net.eval()\n",
    "preds = []\n",
    "with torch.no_grad():\n",
    "    for batch in tqdm(dl_test):\n",
    "        preds.append(net.predict(batch))\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "5c30b8f0",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "yhat_list = [yd.sigmoid().reshape(-1).tolist() for yd in preds]\n",
    "yhat = []\n",
    "for yd in yhat_list:\n",
    "    yhat.extend(yd)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "168cb211",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "dftest_raw = dftest_raw.rename(columns = {target_col: 'y'})\n",
    "dftest_raw['yhat'] = yhat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "6d5e7ab7",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.765697537193438"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.metrics import roc_auc_score\n",
    "roc_auc_score(dftest_raw['y'],dftest_raw['yhat'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e8f8552",
   "metadata": {},
   "source": [
    "## 六，保存模型"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9eeae65b",
   "metadata": {},
   "source": [
    "最佳模型权重已经保存在ckpt_path = 'checkpoint'位置了。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "7eb2899d",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<All keys matched successfully>"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "net.load_state_dict(torch.load('checkpoint',weights_only=True))"
   ]
  }
 ],
 "metadata": {
  "kaggle": {
   "accelerator": "gpu",
   "dataSources": [
    {
     "datasetId": 5446668,
     "sourceId": 9035782,
     "sourceType": "datasetVersion"
    }
   ],
   "dockerImageVersionId": 30747,
   "isGpuEnabled": true,
   "isInternetEnabled": true,
   "language": "python",
   "sourceType": "notebook"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
